package eu.goldenak.ircbot;

import eu.goldenak.ircbot.BotGroup.DistributeMessage;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.util.Random;
import java.util.Vector;

/**
 * One IRC bot.
 */
public class IrcBot {
	/**
	 * The hostname we are going to connect to.
	 */
	private String m_sHostname;
	
	/**
	 * The port we will be using (for IRC the default is 6667).
	 */
	private int m_nPort;
	
	/**
	 * The socket where we will get our data.
	 */
	private Socket m_pSocket;
	
	/**
	 * Pointer to the BufferedReader which we going to use quite a lot.
	 */
	private BufferedReader m_pIn;
	
	/**
	 * The nickname of this bot.
	 */
	private String m_sNickname;
	
	/**
	 * The username (also called ident) of this bot.
	 */
	private String m_sUsername;
	
	/**
	 * The real name, shown in /whois, of this bot. Good place for version info.
	 */
	private String m_sRealname;
	
	/**
	 * Indicates whether this bot is a slave. If so, modules aren't processed,
	 * which means that this bot can be used by a master bot as extra connection
	 * to send messages, to prevent excess flooding.
	 */
	private boolean m_bSlave;
	
	/**
	 * In here we will store the networkname.
	 */
	private String m_sServerName;
	
	/**
	 * This vector keeps track of all channels this bot has joined.
	 */
	private Vector <IrcChannel> m_vChannels;
	
	/**
	 * The optional NickServ password.
	 */
	private String m_sNickServPass;
	
	/**
	 * New style of Irc Data handling.
	 * Example: System.out.println ("User " + pBot.in.nickname + " sent me: " +
	 * 	pBot.in.message);
	 */
	public IrcData in;
	
	/**
	 * Pointer to the IrcHandler object, which is public for easy use.
	 * Example: pBot.out.privmsg ("MrBondt", "Hi, What's up?");
	 */
	public IrcHandler out;
	
	/**
	 * Create a not so useful IrcBot.
	 */
	public IrcBot () {
		this ("default");
	}
	
	/**
	 * Create a slave bot with a specific servername.
	 */
	public IrcBot (String sServerName) {
		setServerName (sServerName);
		setSlave (false);
		m_pSocket = new Socket ();
		m_vChannels = new Vector <IrcChannel> ();
	}
	
	/**
	 * Create a useful IrcBot right away.
	 */
	public IrcBot (String sServerName, String sNickname, String sUsername, String sRealname) {
		this (sServerName);
		
		setNickname (sNickname);
		setUsername (sUsername);
		setRealname (sRealname);
	}
	
	/**
	 * This method will bind the socket to a specific address.
	 */
	public void bind (String sIpAddress) throws IOException {
		bind (sIpAddress, 0);
	}
	
	/**
	 * This method will bind the socket to a specific address and port.
	 */
	public void bind (String sIpAddress, int nPort) throws IOException {
		m_pSocket.bind (new InetSocketAddress (sIpAddress, nPort));
	}
	
	/**
	 * Connect to the IRC server.
	 */
	public void connect (String sHostname, int nPort) throws Exception {
		m_pSocket.connect (new InetSocketAddress (sHostname, nPort));
		m_pSocket.setSoTimeout(1);
		
		m_pIn = new BufferedReader (new InputStreamReader (m_pSocket.getInputStream ()));
		out   = new IrcHandler (new PrintWriter (m_pSocket.getOutputStream (), true));
		
		out.send ("USER " + m_sUsername + " * * :" + m_sRealname);
		out.send ("NICK " + m_sNickname);
	}
	
	/**
	 * Process new messages.
	 */
	public void loop () throws IOException {
		String sLine = read ();
		
		if (sLine != null) {
			onReceive (sLine);
		}
	}
	
	/**
	 * Returns a line from the socket.
	 */
	public String read () throws IOException {
		try {
			String sOut = m_pIn.readLine ();
			//System.out.println (sOut);
			
			if (sOut == null) {
				throw new IOException ("Connection to the IRC server has been lost.");
			}
			
			return sOut;
		} catch (SocketTimeoutException pException) {
			return null;
		}
	}
	
	/**
	 * Closes the socket and everything that depends on it. Also destroys the 
	 * objects.
	 */
	public void close () {
		try {
			out.close ();
			m_pIn.close ();
			m_pSocket.close ();
			
			out       = null;
			m_pIn     = null;
			m_pSocket = null;
		} catch (Exception e) {}
	}
	
	/**
	 * This method will be called every time a message is received from the IRC
	 * server. It will do some processing on it to get some vital data and then
	 * make calls to the ModuleManager, so that the modules can be put to work.
	 */
	public void onReceive (String sLine) {
		if (sLine.charAt (0) != ':' && sLine.charAt (0) != 'P') {
			// Invalid line, ignore.
			return;
		}
		
		in        = new IrcData ();
		in.raw    = sLine;
		in.chunks = sLine.split (" ");
		in.user   = in.chunks [0].substring (1);
		
		int nColonPos = in.raw.indexOf (":", 2);
		if (nColonPos != -1) {
			in.message = in.raw.substring (nColonPos + 1);
		}
		
		int nExPos = in.user.indexOf ("!");
		if (nExPos != -1) {
			String [] aParts = in.user.split ("!|@", 3);
			in.nickname = aParts [0];
			in.username = aParts [1];
			in.hostname = aParts [2];
		} else {
			in.nickname = in.user;
		}
		
		if (in.chunks [0].equals ("PING")) {
			out.send ("PONG :" + in.message);
			return;
		}
		
		ModuleManager pModuleManager = ModuleManager.getInstance ();
		int nCode = 0;
		
		switch (Character.toUpperCase (in.chunks [1].charAt (0))) {
			case '0':
				nCode = Integer.parseInt (in.chunks [1]);
				
				switch (nCode) {
					case 1:
						m_sNickname = in.chunks [2];
						
						// Set the mode as defined in the configfile.
						out.mode (m_sNickname, ConfigManager.getModule ("Main").get ("botmode"));
						pModuleManager.onConnect (this);
						break;
					
					case 5:
						// Some info about the IRC server.
						
						break;
				}
				break;
			
			case '3':
				if (isSlave ()) break;
				
				nCode = Integer.parseInt (in.chunks [1]);
				
				switch (nCode) {
					case 332:
						pModuleManager.onChannelTopic (this, in.chunks [3], in.message);
						break;
					
					case 353:
						pModuleManager.onChannelNames (this, in.chunks [4], in.message);
						break;
				}
				break;
			
			case '4':
				nCode = Integer.parseInt (in.chunks [1]);
				switch (nCode) {
					case 433:
						out.send ("NICK " + in.chunks [3] + new Random ().nextInt (100));
						break;
				}
				break;
			
			case 'I':
				// INVITE
				if (isSlave ()) break;
				
				pModuleManager.onInvite (this, in.nickname, in.chunks [2], in.chunks [3].substring (1));
				break;
			
			case 'J': {
				// JOIN
				String sChannel = in.chunks [2];
				if (sChannel.charAt (0) == ':') {
					sChannel = sChannel.substring (1);
				}
				
				if (in.nickname.equals (m_sNickname)) {
					// The bot has joined a channel
					// FIXME Add channelkey.
					m_vChannels.add (new IrcChannel (sChannel));
				}
				
				if (isSlave ()) break;
				
				pModuleManager.onChannelJoin (this, sChannel, in.nickname);
				break;
			}
			
			case 'K':
				// KICK
				if (isSlave ()) break;
				
				pModuleManager.onChannelKick (this, in.chunks [2], in.chunks [3], in.nickname, in.message);
				break;
			
			case 'M': {
				// MODE
				if (isSlave ()) break;
				
				StringBuilder sMode = new StringBuilder ();
				for (int i = 3; i < in.chunks.length; i ++) {
					sMode.append (in.chunks [i]);
					
					if (i != in.chunks.length - 1) {
						sMode.append (" ");
					}
				}
				
				pModuleManager.onChannelMode (this, in.chunks [2], sMode.toString ());
				break;
			}
			
			case 'N':
				// NICK, NOTICE
				switch (Character.toUpperCase (in.chunks [1].charAt (1))) {
					case 'I':
						// NICK
						if (in.nickname.equals (m_sNickname)) {
							m_sNickname = in.message;
						}
						
						if (isSlave ()) break;
						
						pModuleManager.onChangeNick (this, in.nickname, in.message);
						break;
					
					case 'O':
						// NOTICE
						if (isSlave ()) break;
						
						pModuleManager.onNotice (this, in.chunks [2], in.nickname, in.message);
						break;
				}
				break;
			
			case 'P':
				// PRIVMSG, PART
				switch (Character.toUpperCase (in.chunks [1].charAt (1))) {
					case 'R': {
						// PRIVMSG
						if (isSlave ()) break;
						
						// TODO remove mode characters from channelname (~&@%+).
						String sSource = in.chunks [2];
						
						if (in.message.charAt (0) == 1) {
							// CTCP
							String sType = in.chunks [3].substring (2).toUpperCase ();
							String sMessage = in.message.substring (sType.length () + 2, in.message.length () - 1).trim ();
							
							pModuleManager.onCTCP (this, sSource, in.nickname, sType, sMessage);
						} else {
							if (sSource.charAt (0) == '#') {
								// Channel message
								pModuleManager.onChannelPrivmsg (this, sSource, in.nickname, in.message);
							} else {
								// Private message
								pModuleManager.onPrivmsg (this, in.nickname, in.message);
							}
						}
						break;
					}
					
					case 'A': {
						// PART
						String sChannel = in.chunks [2];
						if (sChannel.charAt (0) == ':') {
							sChannel = sChannel.substring (1);
						}
						
						if (isSlave ()) break;
						
						pModuleManager.onChannelPart (this, sChannel, in.nickname, in.message);
						break;
					}
				}
				break;
			
			case 'T':
				// TOPIC
				if (isSlave ()) break;
				
				pModuleManager.onChangeTopic (this, in.chunks [2], in.nickname, in.message);
				break;
			
			case 'Q':
				// QUIT
				if (isSlave ()) break;
				
				pModuleManager.onQuit (this, in.nickname, in.message);
				break;
			
			default:
				pModuleManager.onUnhandledCommand (this);
				break;
		}
		
		if (!isSlave ()) {
			pModuleManager.onRaw (this, in.raw);
		}
	}
	
	/**
	 * Checks whether this bot is in the specified channel.
	 */
	public boolean isInChannel (String sChannel) {
		return m_vChannels.contains (new IrcChannel (sChannel));
	}
	
	/**
	 * Returns the nickname which the bot is currently connected with.
	 */
	public String getNickname () {
		return m_sNickname;
	}
	
	/**
	 * Sets initial nickname.
	 */
	public void setNickname (String sNickname) {
		m_sNickname = sNickname;
	}
	
	/**
	 * Sets the password to identify with at NickServ.
	 */
	public void setNickservPass(String sNickservPass) {
		m_sNickServPass = sNickservPass;
	}
	
	/**
	 * Returns the real name which is being used by the bot.
	 */
	public String getRealname () {
		return m_sRealname;
	}
	
	/**
	 * Sets the real name, which is shown in a /whois.
	 */
	public void setRealname (String sRealname) {
		m_sRealname = sRealname;
	}
	
	/**
	 * Returns the name for the server to which the bot is currently connected.
	 */
	public String getServerName () {
		return m_sServerName;
	}
	
	/**
	 * Sets the servername to which bot will be connecting.
	 */
	public void setServerName (String sServerName) {
		m_sServerName = sServerName;
	}
	
	/**
	 * Returns the username (or ident) which the bot is currently connected
	 * with.
	 */
	public String getUsername () {
		return m_sUsername;
	}
	
	/**
	 * Sets the initial username (also known as ident).
	 */
	public void setUsername (String sUsername) {
		m_sUsername = sUsername;
	}
	
	/**
	 * If the bot is a slave, then modules will not be executed for this bot.
	 */
	public boolean isSlave () {
		return m_bSlave;
	}
	
	/**
	 * Sets whether this bot is a slave or not.
	 */
	public void setSlave (boolean bSlave) {
		m_bSlave = bSlave;
	}
}